![]() |
![]() |
|
Nehmen wir an, dass ein Thread seine eigene Priorität mit der Eigenschaft Priority erhöhen soll, müssten Sie
codieren, damit der Thread auf sich selbst zugreifen kann. Auf die Eigenschaft Priority kommen wir später zu sprechen. Einen Thread für eine bestimmte Zeitdauer anhaltenIm Beispiel oben wurde eine Schleife eingebaut, um eine kleine Zeitverzögerung zu erreichen. Ohne Schleife könnte es – abhängig von der Taktfrequenz des Prozessors – sein, dass die gesamte Schleife des ersten Threads bereits vollständig abgearbeitet ist, bevor der zweite Thread zum ersten Mal in seine eigene Schleife eintritt. Die Thread-Klasse bietet für solche Fälle mit der Methode Sleep eine bessere Alternative, einen Thread für eine bestimmte Zeitdauer anzuhalten und damit die Ausführung zu verzögern.
Beachten Sie, dass diese Methoden statisch definiert sind und nicht auf eine bestimmte Threadinstanz aufgerufen werden können, sondern nur aus dem Code des aktuell laufenden Threads heraus, der sich damit selbst aus dem Verkehr zieht. Im Gegensatz zu den im ersten Beispiel verwendeten Schleifen zur Simulation einer länger andauernden Threadoperation ist Sleep unabhängig von der Taktfrequenz des Computers. Nur der Thread selbst kann bestimmen, ob er sich selbst zur Ruhe legt oder nicht, ein anderer Thread hat keinen Einfluss darauf. Sie rufen Sleep auf, indem Sie entweder die Anzahl der Millisekunden angeben, die sich der Thread zurückziehen soll, oder Sie übergeben eine Referenz auf ein TimeSpan-Objekt. Den Effekt, den wir mit Thread.Sleep erzielen können, wollen wir uns an einem Beispiel ansehen.
Auch in diesem Beispiel wird eine Methode MyProcedure in einem zweiten Thread ausgeführt. Der Primärthread wird 20 ms stillgelegt, der Sekundärthread für jeweils nur 5 ms pro Schleifendurchlauf. Damit erhält der Sekundärthread circa viermal so viel Prozessorzeit wie der Primärthread, was sich auch an der Konsole zeigt (siehe Abbildung 11.3).
Abbildung 11.3 Konsolenausgabe des Beispiels »SleepingThread« Bisher wissen Sie, dass Sleep eine Zeitspanne in Form eines Integers oder einer Referenz auf ein TimeSpan-Objekt übergeben wird, um die Ruhezeit des Threads zu beschreiben. Es gibt noch zwei weitere Möglichkeiten, Sleep einzusetzen, was ein bisher noch nicht erörtertes Verhalten aufzwingt: 1. die Übergabe des Wertes 0
Wird Sleep die Zahl 0 übergeben, wird der Thread dazu veranlasst, auf den verbleibenden Rest seiner Ausführungszeit zu verzichten und die CPU für den nächsten anstehenden Thread frei zu machen. Er reiht sich danach sofort wieder in die Warteschlange ein. Wartende ThreadsEin Thread, der sich auf eine unbestimmte Zeit zur Ruhe begibt, ruft die Sleep-Methode mit dem Wert Timeout.Infinite auf, also:
Die Klasse Timeout ist wohl so mager ausgestattet wie kaum eine andere des .NET Frameworks. Sie enthält nur die Konstante Infinite, die den Wert –1 repräsentiert. Ein auf diese Weise eingefrorener Thread kommt nicht mehr automatisch zum Zuge, wenn es darum geht, vom Scheduler ein Stück Zeitscheibe zu erhaschen, denn er ist nicht bereit, sondern er wartet. Dieser Zustand kann nur durch einen anderen Thread aufgehoben werden. Dazu ruft der aktive Thread die Methode Interrupt auf den wartenden Thread auf:
Die Sleep-Methode ist also leistungsfähiger, als es im ersten Augenblick den Anschein hat. Fassen wir die möglichen Argumente noch einmal kurz in tabellarischer Form zusammen:
Einem anderen Thread die Zeitscheibe entziehenMit der Methode Sleep kann sich ein Thread selbst einfrieren, auf einen anderen Thread kann diese Methode nicht aufgerufen werden. Soll ein Thread einem anderen die Zeitscheibe entziehen, muss er die Methode Suspend auf den entsprechenden Thread aufrufen:
Ein Thread kann diese Methode auch auf sich selbst aufrufen. Das kommt dann der Übergabe der Konstanten Timeout.Infinite an Sleep gleich. Der Aufruf von Suspend auf einen Thread, der seine Ausführung beendet hat oder noch nicht gestartet worden ist, führt zu der Ausnahme ThreadStateException. Besteht die Gefahr, auf einen solchen Thread zu treffen, muss der Zustand des betreffenden Threads zuvor ermittelt werden. Dazu greift man auf ein Member der Enumeration ThreadState zurück.
Die Member der Enumeration ThreadState werden bitweise kombiniert. Wollen Sie sicherstellen, dass beim Aufruf von Suspend keine Ausnahme ausgelöst wird, müssen Sie daher, wie nachfolgend gezeigt, den Zustand des Threads abfragen:
Ein Thread, dem mit Suspend die Zeitscheibe entzogen wurde, muss wieder angestoßen werden, bevor er seine ihm zugedachte Aufgabe weiter bearbeiten kann. Dazu wird die Methode Resume auf die Referenz des wartenden Threads aufgerufen. Auf einen Thread darf mehrfach Suspend ausgeführt werden, ohne dass es zu einer Fehlermeldung kommt. Allerdings reicht ein einziges Resume, um den Thread wieder in die Warteschlange auf die Zeitscheibe einzureihen. Im folgenden Beispielprogramm wird der Einsatz der Methoden Suspend und Resume demonstriert. In der darauf folgenden Abbildung 11.4. ist die Ausgabe des Programms zu sehen.
Abbildung 11.4 Die Konsolenausgabe des Beispiels »SuspendThread« Der Aufruf von Suspend oder Resume auf einen Thread, der nicht gestartet oder bereits beendet worden ist, führt zur Ausnahme ThreadStateException. Dieselbe Ausnahme wird auch ausgelöst, falls Resume auf einen Thread trifft, der nicht suspendiert worden ist. Sicheres Beenden eines ThreadsEinem Thread auf eine bestimmte oder unbestimmte Zeit die Zeitscheibe zu entziehen, ist eine Sache. Eine andere ist das Terminieren eines Threads mit der Methode Thread.Abort. Der Aufruf bewirkt in der Laufzeitschicht die Auslösung der Ausnahme ThreadAbortException. Damit ist es möglich, die Methode ordnungsgemäß zu beenden, beispielsweise um dabei offene Ressourcen zu schließen. Dazu zunächst ein Beispiel. Diesmal wird die Routine, die in einem zweiten Thread ausgeführt wird, in einer eigenen Klasse definiert. Damit ändert sich grundsätzlich nichts, da dem Delegaten nun die Adresse der Instanzmethode in der Klasse mitgeteilt wird.
Sehen wir uns zuerst den Programmcode an. Nach dem Instanziieren der Thread-Klasse wird der zweite Thread gestartet. Da wir die Abort-Methode testen wollen, müssen wir dafür sorgen, dass Abort nicht auf einen Thread trifft, der nicht mehr ausgeführt wird. Deshalb ist in ThreadExecution der Klasse ClassA eine Schleife eingebaut, die eine längere Zeit für einen vollständigen Durchlauf benötigt. Die Zeit muss so groß angesetzt werden, dass Abort auf die sich noch in Arbeit befindliche Schleife trifft. Vor dem Aufruf von Abort wird der Primärthread zunächst mit Sleep gebremst, damit der Sekundärthread etwas Zeit zu arbeiten hat. Nach dem Aufruf von Abort bekommt das System mit einem zweiten Sleep-Aufruf noch Zeit, den Sekundärthread endgültig zu beenden. Durch Auswertung der Eigenschaft IsAlive auf dem Sekundärthread wird festgestellt, ob dieser noch aktiv ist oder nicht. Würden wir dem Hauptthread keine Ruhepause gönnen, könnte eine falsche Aussage die Folge sein, da die if-Bedingungsprüfung vor der Aufgabe des Sekundärthreads durchgeführt wird, weil sich Abort und if innerhalb derselben Zeitscheibe befinden und der freigegebene Thread noch keine Möglichkeit erhalten hat, die Ausnahme auszulösen. Die zweite Schleife in der Methode ThreadExecution der Klasse ClassA soll ebenfalls eine länger andauernde Operation simulieren. An der Konsole erfolgt die folgende Ausgabe:
Festzustellen ist ein anscheinender Widerspruch zu der Aussage in Kapitel 9, dass die hinter finally stehenden Anweisungen ausgeführt werden: Der Aufruf von Abort löst die Exception ThreadAbortException aus, aber die zweite Schleife im Sekundärthread wird nicht mehr durchlaufen. Genau in diesem Punkt liegt das Besondere dieser Ausnahme, denn sie wird ausgelöst und auch gefangen, aber die Anweisungen hinter dem Ende der Ausnahmebehandlung kommen nicht mehr zur Ausführung, da der Thread in diesem Moment bereits terminiert ist. Allerdings unterstützt die Laufzeitschicht abschließende Anweisungen in finally. Gegen das außerplanmäßige Beenden kann sich der betroffene Thread allerdings auch zur Wehr setzen. Dazu muss im catch-Block des Exceptionhandlers die statische Methode ResetAbort aufgerufen werden:
Bauen Sie diese Anweisung in den Programmcode des Beispiels ein, wird auch die zweite Schleife in ThreadExecution ausgeführt, und die bedingte Prüfung mit if führt zu dem Ergebnis, dass der Thread noch lebt – das allerdings auch nur, weil die zweite Schleife ebenfalls wieder eine längere Zeit in Anspruch nimmt oder der Thread nicht schon auf normalen Wege aufgegeben worden ist, bevor die Prüfung erfolgt. Abhängige Threads – die Methode »Join«Nun wäre die folgende Ausgangssituation vorstellbar: Der Primärthread beendet den Sekundärthread mit Abort und muss dabei sicherstellen, dass die Anweisungen im Sekundärthread zuerst vollständig abgearbeitet sind, bevor die nächste Anweisung im Primärthread ausgeführt wird. Solche Situationen können auftreten, wenn der Code des Primärthreads auf das ordnungsgemäße Beenden angewiesen ist. Das heißt aber auch, dass der Aufruf synchron erfolgen muss, also auf die quasi gleichzeitige Ausführung, die ansonsten die Threads auszeichnet, bewusst verzichtet wird. Wir wollen, um uns der Problematik bewusst zu werden, zunächst eine kleine Änderung in Main vornehmen. Die Implementierung der Klasse ClassA bleibt wie im Beispiel AbortThread erhalten (also ohne den Aufruf von ResetAbort, falls Sie damit experimentiert haben sollten).
Main enthält eine Anweisung, die nach dem Aufruf der Abort-Methode die Konsolenausgabe
erzwingt. Damit sollen Anweisungen simuliert werden, die auf das ordnungsgemäße Terminieren des sekundären Threads angewiesen sind. Sehen wir uns zunächst die Konsolenausgabe des Programmcodes in Abbildung 11.5 an.
Abbildung 11.5 Abhängige Threads – unerwünschter Programmfluss Deutlich ist zu erkennen, dass der sekundäre Thread nach Abort immer noch aktiv ist – die catch- und finally-Blöcke werden nach der abhängigen Anweisung ausgeführt. Jetzt hilft eine andere Methode der Klasse Thread weiter: Join, die den aktuellen, also aufrufenden Thread so lange blockiert, bis der Sekundärthread vollständig terminiert ist. Sinnvollerweise wird Join direkt hinter Abort aufgerufen. Der Programmablauf kehrt erst dann zum Aufrufer zurück, wenn die Threadausführung ordnungsgemäß beendet ist.
Wenn Sie dieses Programm starten, gibt die Konsole das folgende Ergebnis aus:
Abbildung 11.6 Ausgabe nach dem sicheren Beenden des Threads Vergleichen wir diese Ausgabe mit der, die wir ohne Join hatten (Abbildung 11.5), können wir eindeutig erkennen, dass der Thread, dessen Terminierung angestoßen wurde, zuerst vollständig abgearbeitet wird, bevor der Aufrufer seinen eigenen Programmfluss fortsetzt. Thread-Prioritäten festlegenJeder Thread hat eine Priorität. Mit der Eigenschaft Priority lässt sich die Priorität eines Threads erhöhen, verringern oder einfach nur auswerten. Die Priorität spielt eine entscheidende Rolle bei der Vergabe der Zeitscheibe: Ein Thread hat Vorrang vor einem anderen Thread mit niedrigerer Priorität – vorausgesetzt natürlich, dass sich beide durch den Zustand bereit beschreiben lassen. Priority ist vom Typ der Enumeration ThreadPriority, die fünf Member definiert:
Die Prioritäten können von der höchsten Stufe (Threadpriority.Highest) bis zur niedrigsten (ThreadPriority.Lowest) eingestellt werden. Die automatisch einem Thread zugewiesene Priorität lautet Normal. Der Thread mit der höchsten Priorität erhält die Zeitscheibe und läuft so lange, bis
Am häufigsten ist der Fall anzutreffen, dass sich mehrere Threads gleicher Priorität in die Warteschlange zur CPU eingeordnet haben. Alle erhalten gleiche Zeitanteile nach einem Verfahren, das als Round-Robin-Verteilungsverfahren bezeichnet wird. Dabei wird karussellartig die Zeitscheibe auf die bereiten Threads verteilt. Im folgenden Beispielprogramm wollen wir die Auswirkungen der Prioritätsfestlegung in einer Anwendung studieren.
Die im Kontext unserer Problemstellung wichtigen Codezeilen sind:
Um den Unterschied deutlich zu machen, empfiehlt es sich, beim ersten Versuch die Anweisung zur Erhöhung der Priorität des ersten Threads auszukommentieren. Starten Sie mit dieser Vorgabe die Laufzeit, werden Sie eine Konsolenausgabe wie in Abbildung 11.7 gezeigt erhalten. thread1 wird gestartet, schreibt ein paar Punkte in die Ausgabe und übergibt danach dem Prozessor den thread2, der sich durch eine eigene Zeichenfolge bemerkbar macht. Die Zeitscheibe dauert lang genug, um die Anweisungen von thread2 vollständig zu bearbeiten. Danach übernimmt wieder thread1 die CPU und beendet seine Ausführung.
Abbildung 11.7 Konsolenausgabe ohne Hochsetzen der Priorität Erhöhen wir nun die Priorität des ersten Threads um eine Stufe und starten ein zweites Mal die Laufzeit.
Abbildung 11.8 Konsolenausgabe nach dem Hochsetzen der Priorität Da thread1 nun die höchste Priorität aller sich in der Warteschlange befindlichen Threads hat (besser gesagt, ist zumindest die Priorität im Vergleich zu thread2 höher), werden seine Anweisungen zuerst vollständig ausgeführt, bevor thread2 mit seiner geringeren Priorität an die Reihe kommt. Einem Thread eine gewisse Sonderstellung durch die Erhöhung der Priorität einzuräumen, mag vielleicht manchmal ganz verlockend klingen. Bedenken Sie jedoch, dass dieser Thread bei einer lang andauernden Operation eine bremsende Wirkung auf die anderen Threads hat. Man spricht auch von einem Aushungern des Systems. Gehen Sie daher sorgfältig mit dem Erhöhen von Prioritäten um, und achten Sie darauf, dass keine unnötigen Operationen von einem solchen Thread ausgeführt werden, sondern nur solche, die für den weiteren Ablauf der Anwendung unbedingt notwendig sind. Vorder- und HintergrundthreadsThreads werden in zwei Kategorien unterteilt: in Vorder- und in Hintergrundthreads. Ein Prozess wird ausgeführt, solange noch mindestens ein Vordergrundthread existiert. Mit dem Beenden des letzten Vordergrundthreads wird der Prozess der Anwendung selbst dann beendet, wenn Hintergrundthreads noch aktiv sind und die ihnen auferlegte Aufgabe noch nicht vollständig ausgeführt haben. Die Eigenschaft IsBackground beschreibt, ob ein Thread als Vorder- oder Hintergrundthread eingestuft ist. Grundsätzlich sind alle Threads, die aus der Klasse Thread erzeugt werden, zunächst Vordergrundthreads. Mit IsBackground lässt sich ein Thread aber auch zu einem Hintergrundthread degradieren. Im folgenden Beispielprogramm ist der Effekt des Unterschieds zwischen einem Vorder- und Hintergrundthread deutlich zu erkennen. In Main wird ein neuer Thread erzeugt und als Hintergrundthread festgelegt. Main läuft selbst in einem Vordergrundthread terminiert, bevor der Hintergrundthread seine Aufgabe vollständig ausgeführt hat – die Ausgabe des Hintergrundthreads an der Konsole ist unvollständig. Kommentieren Sie die Anweisung aus, in welcher der zweite Thread zum Hintergrundthread wird, wird der Prozess erst in dem Moment beendet, wenn beide Threads die ihnen zugestandene Aufgabe abgeschlossen haben.
11.2.2 Threadpools nutzen
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // -------------------------------------------------------------- |
| // Beispiel: ...\ Kapitel 11\ThreadpoolDemo |
| // -------------------------------------------------------------- |
| class Program { |
| static void Main(string[] args) { |
| // den Threadpool erforschen |
| int maxThreads; |
| int asyncThreads; |
| ThreadPool.GetMaxThreads(out maxThreads, out asyncThreads); |
| Console.WriteLine("Max. Anzahl Threads: {0}", maxThreads); |
| Console.WriteLine("Max. Anzahl E/A-Threads: {0}", asyncThreads); |
| Console.WriteLine(new string('-', 40)); |
| // Benachrichtigungsereignis, Zustand 'nicht signalisieren' |
| AutoResetEvent ready = new AutoResetEvent(false); |
| // Anfordern eines Threads aus dem Pool |
| ThreadPool.QueueUserWorkItem(new WaitCallback(Calculate), ready); |
| Console.WriteLine("Der Hauptthread wartet ..."); |
| // Hauptthread in den Wartezustand setzen |
| ready.WaitOne(); |
| Console.WriteLine("Sekundärthread ist fertig."); |
| Console.ReadLine(); |
| } |
| public static void Calculate(object obj) { |
| Console.WriteLine("Im Sekundärthread"); |
| Thread.Sleep(5000); |
| // Ereigniszustand auf 'signalisieren' festlegen |
| ((AutoResetEvent)obj).Set(); |
| } |
| } |
Die Methode Calculate soll in einem Thread aus dem Threadpool ausgeführt werden. Bevor diese Operation eingeleitet wird, wollen wir aber noch feststellen, wie viele Threads uns der Pool zur Verfügung stellt, und rufen die statische Methode GetMaxThreads auf. Über den ersten Parameter werden uns die Threads geliefert, der zweite Parameter gibt darüber hinaus Auskunft über die maximale Anzahl der möglichen E/A-Anforderungen. Sie werden feststellen, dass sich 25 Threads im Pool befinden, und zwar pro Prozessor.
Das Beispiel ist so entwickelt, dass nicht nur ein Thread aus dem Pool zur Ausführung der Methode Calculate herangezogen wird. Darüber hinaus wird auch ein Synchronisationsszenario in Gang gesetzt, das bewirkt, dass während der Ausführung von Calculate der aufrufende Code in Wartestellung versetzt wird und auf ein Signal von Calculate wartet, bevor er seine Arbeit wieder aufnimmt. Mehr zur Synchronisierung erfahren Sie im folgenden Abschnitt.
Dem Aufruf der statischen Methode QueueUserWorkItem wird ein Delegat, der die im Thread auszuführende Methode beschreibt, übergeben. Darüber hinaus kann QueueUserWorkItem ein zweites Argument übergeben werden, um der Threadmethode Daten bereitzustellen. Hier wird dem zweiten Parameter ein Objekt vom Typ AutoResetEvent übergeben. Dieses Objekt versetzt zwei Threads in die Lage, über Signale miteinander zu kommunizieren. Erzeugt wird das Objekt im Code mit:
| AutoResetEvent ready = new AutoResetEvent(false); |
Der Übergabeparameter false besagt, dass der anfängliche Zustand des Objekts auf »nicht signalisiert« festgelegt wird. Mit
| ready.WaitOne(); |
wird der aktuelle Thread so lange blockiert, bis er ein Signal erhält. Dieses stammt aus der Threadmethode und wird durch Aufruf der Set-Methode des AutoResetEvent-Objekts ausgelöst:
| ((AutoResetEvent)obj).Set(); |
Hier profitieren wir davon, der Threadmethode im zweiten Parameter die Referenz auf das AutoResetEvent übergeben zu haben.
| << zurück |
|
||||||||||||||
|
||||||||||||||
|
||||||||||||||
|
||||||||||||||
Copyright © Galileo Press 2006
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.